/**
 * @file main.c
 * @brief md5bsum - An advanced program to check files sum with md5.
 * @author Julien Folly
 * @version $Revision: 196 $
 * @date $Date: 2013-07-09 12:01:03 +0200 (Di, 09 Jul 2013) $
 * @copyright Julien Folly
 */

#define _GNU_SOURCE /* memrchr() */

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <openssl/md5.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define L_NOSUMREAD		0x01
#define L_NOSUMCOMP		0x02
#define L_STATERR		0x04
#define L_OPENDIRERR	0x08
#define L_OPENFILEERR	0x10
#define L_NOSUM			(L_NOSUMREAD | L_NOSUMCOMP)

/**
 * Chain element to load all filenames with attributes and checksum.
 */
typedef struct s_md5{
	char * path;
	struct stat sb;
	char flag;
	unsigned char sumread[MD5_DIGEST_LENGTH];
	unsigned char sumcomp[MD5_DIGEST_LENGTH];
	struct s_md5 * next;
}t_md5;

/** The list start pointer */
t_md5 *list_start = NULL;

/** The list end pointer */
t_md5 *list_end = NULL;

/**
 * Append an element to the general list files.
 * @param pathlen Memory to allocate for e->path
 * @return The new last element or NULL if error
 */
t_md5 * list_append(size_t pathlen){
	t_md5 *e;

	/* Malloc a new element */
	e = (t_md5 *)malloc(sizeof(t_md5));
	if(e == NULL) return NULL;

	/* Malloc path */
	e->path = (char *)malloc(pathlen);
	if(e->path == NULL){
		free(e);
		return NULL;
	}

	/* Chain it */
	if(list_end != NULL) list_end->next = e;
	list_end = e;
	e->next = NULL;
	if(list_start == NULL) list_start = e;

	/* Set flags */
	e->flag = L_NOSUMREAD | L_NOSUMCOMP;

	/* Return list_end */
	return e;
}

/**
 * Get the "struct stat sb" of "e->path"
 * @param e The element
 * @return Set flag L_ERRSTAT if error
 */
void list_statfile(t_md5 *e){
	/* Get info about the file */
	if(lstat(e->path, &e->sb) == -1) e->flag |= L_STATERR;
}

/**
 * Add file to the list, if it's a folder and recursive is set to true
 * go into the sub-folder.
 * @param path Path to file
 * @param path2 Path to concatenate with path, could be NULL.
 * @param recursive Do we open sub folder.
 * @return Return false on allocation error, for others errors see e->flag.
 */
bool list_addfile(const char * path, const char * path2, bool recursive){
	DIR *dirp;
	struct dirent *dp;
	t_md5 *e;

	/* Malloc a new element and set path */
	if(path2 != NULL){
		e = list_append(strlen(path)+strlen(path2)+2);
		if(e == NULL) return false;
		sprintf(e->path, "%s/%s", path, path2);
	}else{
		e = list_append(strlen(path)+1);
		if(e == NULL) return false;
		strcpy(e->path, path);
	}

	/* Stat */
	list_statfile(e);

	/* Do we open directory ? */
	if((e->flag & L_STATERR) == 0 && S_ISDIR(e->sb.st_mode) && recursive){
		dirp = opendir(e->path);
		if(dirp == NULL){
			e->flag |= L_OPENDIRERR;
			return e;
		}
		while((dp = readdir(dirp)) != NULL){
			if(strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) continue;
			if(!list_addfile(e->path, dp->d_name, recursive)) return false;
		}
		closedir(dirp);
	}
	return true;
}

/**
 * Make the checksum for a file element
 * @param e File element
 * @param follow Do we follow link?
 * @param nonreg Do we open non-regular files?
 * @param hashrights Do we add file rights to the hash?
 * @remark Remove L_NOSUMCOMP if no error
 */
void list_sumElem(t_md5 *e, bool follow, bool nonreg, bool hashrights){
	MD5_CTX c;
	char * link, buf[256];
	int f;
	ssize_t s;
	bool emptyhash = true;

	/* Check stat */
	if(e->flag & L_STATERR) return;

	/* Init MD5 */
	if(!MD5_Init(&c)) return;

	/* Add rights to the hash if necessary */
	if(hashrights){
		if(!MD5_Update(&c, &e->sb.st_uid, sizeof(e->sb.st_uid))) return;
		if(!MD5_Update(&c, &e->sb.st_gid, sizeof(e->sb.st_gid))) return;
		if(!MD5_Update(&c, &e->sb.st_mode, sizeof(e->sb.st_mode))) return;
		emptyhash = false;
	}

	/* If it's a link and not follow */
	if(S_ISLNK(e->sb.st_mode) && !follow){
		link = (char *)malloc(e->sb.st_size);
		if(link == NULL) return;
		readlink(e->path, link, e->sb.st_size);
		if(!MD5_Update(&c, link, e->sb.st_size)) return;
		free(link);
		emptyhash = false;
	}else

	/* If it's a regular file or we want to read all type */
	if(S_ISREG(e->sb.st_mode) || (nonreg && !S_ISDIR(e->sb.st_mode))){
		/* Open file */
		f = open(e->path, O_RDONLY);
		if(f == -1){
			e->flag |= L_OPENFILEERR;
			return;
		}

		/* Read file and sum */
		while((s = read(f, buf, sizeof(buf))) > 0){
			if(!MD5_Update(&c, buf, s)) return;
		}

		/* Close */
		close(f);
		emptyhash = false;
	}

	/* Add hash into the list */
	if(!MD5_Final(e->sumcomp, &c)) return;

	/* If all went successfully, remove L_NOSUMCOMP flag */
	if(!emptyhash) e->flag &= ~L_NOSUMCOMP;
}

/**
 * Compute all sum of the list
 * @param follow Do we follow link?
 * @param nonreg Do we open non-regular files?
 * @param hashrights Do we add file rights to the hash?
 * @remark Remove L_NOSUMCOMP if no error
 */
void list_sum(bool follow, bool nonreg, bool hashrights){
	t_md5 *e = list_start;

	while(e != NULL){
		list_sumElem(e, follow, nonreg, hashrights);
		e = e->next;
	}
}

/**
 * Save the list into a file.
 * Use the the computed sum, if available otherwise the read sum.
 * If the read sum is also none available, it will not be saved
 * @param filename Where to save
 * @return true if OK
 */
bool list_save(const char * filename){
	int f;
	short int len;
	t_md5 *e = list_start;

	f = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0666);
	if(f == -1)	return false;

	while(e != NULL){
		if((e->flag & L_NOSUM) != L_NOSUM){
			len = strlen(e->path);

			/* Write file */
			write(f, &len, sizeof(len));
			write(f, e->path, len);
			if((e->flag & L_NOSUMCOMP) == 0)
				write(f, e->sumcomp, sizeof(e->sumcomp));
			else
				write(f, e->sumread, sizeof(e->sumread));
		}
		e = e->next;
	}

	/* Close */
	close(f);

	return true;
}

/**
 * Add element to the list from the file
 * @param filename From Where to load
 * @return true if OK
 */
bool list_load(const char * filename){
	short int len;
	t_md5 *e;
	int f;

	/* Open file */
	f = open(filename, O_RDONLY);
	if(f == -1)	return false;

	/* Read file and sum */
	while(read(f, &len, sizeof(len)) > 0){
		/* Malloc a new element */
		e = list_append(len+1);
		if(e == NULL) return false;

		/* Read path and checksum */
		read(f, e->path, len);
		e->path[len] = 0;
		read(f, e->sumread, sizeof(e->sumread));

		/* Stat */
		list_statfile(e);

		/* If all went successfully, remove L_NOSUMREAD flag */
		e->flag &= ~L_NOSUMREAD;
	}

	/* Close */
	close(f);

	return true;
}

/**
 * Print the list
 */
void list_print(){
	t_md5 *e = list_start;
	int i;
	const char * unknow = "???????????????????????????????? ";

	printf("---------- Read hash ----------- -------- Computed hash --------- File\n");
	while(e != NULL){
		if(!(e->flag & L_NOSUMREAD)){
			for(i=0;i<16;++i) printf("%02x", e->sumread[i]);
			printf(" ");
		}else printf(unknow);
		if(!(e->flag & L_NOSUMCOMP)){
			for(i=0;i<16;++i) printf("%02x", e->sumcomp[i]);
			printf(" ");
		}else printf(unknow);
		printf("%s\n", e->path);

		e = e->next;
	}
}

/**
 * List error
 * @return Return the number of error
 */
unsigned int list_printError(){
	t_md5 *e = list_start;
	unsigned int c = 0;

	while(e != NULL){
		if(e->flag & L_OPENDIRERR){
			fprintf(stderr, "Cannot open directory %s\n", e->path);
			++c;
		}
		if(e->flag & L_OPENFILEERR){
			fprintf(stderr, "Cannot open file %s\n", e->path);
			++c;
		}
		if(e->flag & L_STATERR){
			fprintf(stderr, "Cannot lstat file %s\n", e->path);
			++c;
		}
		e = e->next;
	}

	return c;
}

/**
 * Free memory
 */
void list_free(){
	t_md5 *tofree, *e = list_start;

	while(e != NULL){
		tofree = e;
		e = e->next;
		free(tofree->path);
		free(tofree);
	}

	list_start = list_end = NULL;
}

/**
 * Check if hashes are different from the computed one and the read one.
 * @return Return the number of files that differ
 */
unsigned int list_check(){
	t_md5 *e = list_start;
	unsigned int c = 0;

	while(e != NULL){
		if((e->flag & (L_NOSUMCOMP | L_NOSUMREAD)) ||
				memcmp(e->sumcomp, e->sumread, MD5_DIGEST_LENGTH) != 0){
			printf("%s differ\n", e->path);
			++c;
		}
		e = e->next;
	}
	if(c==0) printf("All OK - No hash difference.\n");
	return c;
}


/**
 * Transform a path into a canonical form
 * @param src path to canonicalize
 * @return The new path - must be freed
 */
char * canonicalize_path(const char * src) {
	char *cwd, *dest, *slash, *prevslash;
	size_t pos, len;

	/* Make absolute path with relative path */
	if(src[0] == 0 || src[0] != '/'){
		cwd = getcwd(NULL, 0);
		if(cwd == NULL) return NULL;
		pos = strlen(cwd);
		dest = (char *)malloc(pos+strlen(src)+2);
		if(dest == NULL) return NULL;
		strcpy(dest, cwd);
		free(cwd);
	}else{
		pos = 0;
		dest = (char *)malloc(strlen(src)+1);
		if(dest == NULL) return NULL;
	}

	/* Remove //, /./ and /../ */
	do{
		slash = strchrnul(src, '/');
		len = slash-src;

		switch(len) {
			case 2:
				if(src[0] == '.' && src[1] == '.') {
					prevslash = memrchr(dest, '/', pos);
					if (prevslash != NULL) pos = prevslash - dest;
					goto cont;
				}
				break;
			case 1:
				if(src[0] == '.') goto cont;
				break;
			case 0:
				goto cont;
		}

		dest[pos++] = '/';
		memcpy(&dest[pos], src, len);
		pos += len;
		cont:
		src=slash+1;
	}while(*slash != 0);
	dest[pos] = 0;

	return dest;
}

/**
 * Remove a file from the list
 * @param filename
 * @return true if OK
 */
bool list_remove(char * filename){
	t_md5 *e = list_start, *en, *prev = NULL;
	char *p1, *p2;
	while(e != NULL){
		p1 = canonicalize_path(e->path);
		p2 = canonicalize_path(filename);
		if(p1==NULL || p2 == NULL) return false;
		if(strcmp(p1, p2) == 0){
			if(e == list_start) list_start = e->next;
			else prev->next = e->next;
			if(e == list_end) list_end = prev;
			en = e->next;
			free(e->path);
			free(e);
			e = en;
		}else{
			prev = e;
			e = e->next;
		}
		free(p1);
		free(p2);
	}
	return true;
}

/**
 * Remove a portion of the path
 * @param prefix The prefix to remove
 * @return true if OK
 */
bool list_rmprefix(char * prefix){
	t_md5 *e = list_start;
	size_t plen = strlen(prefix);
	while(e != NULL){
		if(strncmp(e->path, prefix, plen) == 0)
			strcpy(e->path, e->path+plen);
		e = e->next;
	}
	return true;
}

/**
 * Redo the checksum only for a given file
 * @param filename
 * @return true if OK
 */
bool list_update(char * filename, bool follow, bool nonreg, bool hashrights){
	t_md5 *e = list_start;
	char *p1, *p2;
	while(e != NULL){
		p1 = canonicalize_path(e->path);
		p2 = canonicalize_path(filename);
		if(p1==NULL || p2 == NULL) return false;
		if(strcmp(p1, p2) == 0){
			list_sumElem(e, follow, nonreg, hashrights);
		}
		e = e->next;
		free(p1);
		free(p2);
	}
	return true;
}

/**
 * Print usage on terminal.
 * @param progname Should be argv[0]
 */
void usage(const char * progname){
	printf(
			"Usage: %s [OPTIONS]\n"
			"  --add [FILES]\t\tAdd files into the list\n"
			"  --sum\t\t\tHash all files in list\n"
			"  --save [FILE]\t\tSave the list into a binary file\n"
			"  --load [FILES]\tLoad one or more list from files\n"
			"  --check\t\tCheck if the computed hashes are the same as the loaded\n"
			"  --print\t\tPrint the files list with hashes\n"
			"  --perror\t\tPrint all file and directory opening error\n"
			"  --rm [FILES]\t\tRemove a file from the list\n"
			"  --rmprefix [PATHS]\tRemove path prefix on files from the list\n"
			"  --update [FILES]\tMake the hash of theses files only\n"
			"  --follow\t\tFollow symlink and don't make hash of the symlink\n"
			"  --nonreg\t\tAlso hash non-regular files\n"
			"  --hashrights\t\tAdd file uid, gid and mode to the hash\n"
			"  --recursive\t\tDive into sub-folders\n"
			"  --help\t\tThis help\n\n"
			"Exemple:\n"
			"  Make hashes of configuration files:\n"
			"    --recursive --add /etc --sum --save config.md5b\n\n"
			"  Check configuration files:\n"
			"    --load config.md5b --sum --check\n\n"
			"  Add a file into the hashes file (do not update other hashes):\n"
			"    --add /etc/newfile --sum --load config.md5b --save config.md5b\n\n"
			"  Add a file into the hashes file (do update other hashes):\n"
			"    --add /etc/newfile --load config.md5b --sum --save config.md5b\n\n"
			"  Recompute all hashes:\n"
			"    --load config.md5b --sum --save config.md5b\n\n"
			"  Recompute just one file hash:\n"
			"    --load config.md5b --update /etc/modfile --save config.md5b\n\n"
			"  Remove one file:\n"
			"    --load config.md5b --rm /etc/delfile --save config.md5b\n\n"
			"  Check ignoring some files:\n"
			"    --load config.md5b --rm /etc/ignfile1 /etc/ignfile2 --sum --check\n\n"
			"Return:\n"
			"  With --check the program returns the number of mismatching hashes.\n"
			"  With --perror the program returns the number of opening errors.\n"
			"  With both options, the program returns an addition of both results.\n"
			"  Missing file or reading error leads to a mismatched hash.\n"
			"  For others fatals error (memory allocation) the program returns -1.\n\n"
			"Version 1.0 - julien.folly[at]gmail[dot]com\n\n", progname);
}

/**
 * Program main operation mode.
 */
typedef enum{
	m_add, m_save, m_load, m_remove, m_rmprefix, m_update, m_undefined
}t_pmode;

/**
 * Main function
 * @param argc Number of command line arguments
 * @param argv Arguments
 * @return 0 = OK
 */
int main(int argc, char * argv[]){
	int i, r = 0;
	bool follow = false;
	bool nonreg = false;
	bool hashrights = false;
	bool recursive = false;
	t_pmode m = m_undefined;

	if(argc <= 1){
		usage(argv[0]);
		return -1;
	}

	for(i=1;i<argc;++i){
		if     (strcmp(argv[i],"--add") == 0) m = m_add;
		else if(strcmp(argv[i],"--sum") == 0) list_sum(follow, nonreg, hashrights);
		else if(strcmp(argv[i],"--save") == 0) m = m_save;
		else if(strcmp(argv[i],"--load") == 0) m = m_load;
		else if(strcmp(argv[i],"--check") == 0) r += list_check();
		else if(strcmp(argv[i],"--print") == 0) list_print();
		else if(strcmp(argv[i],"--perror") == 0) r += list_printError();
		else if(strcmp(argv[i],"--rm") == 0) m = m_remove;
		else if(strcmp(argv[i],"--rmprefix") == 0) m = m_rmprefix;
		else if(strcmp(argv[i],"--update") == 0) m = m_update;
		else if(strcmp(argv[i],"--follow") == 0) follow = true;
		else if(strcmp(argv[i],"--nonreg") == 0) nonreg = true;
		else if(strcmp(argv[i],"--hashrights") == 0) hashrights = true;
		else if(strcmp(argv[i],"--recursive") == 0) recursive = true;
		else if(strcmp(argv[i],"--help") == 0){
			usage(argv[0]);
			return -1;
		}else{
			switch(m){
				case m_add:
					if(!list_addfile(argv[i], NULL, recursive)){
						perror("Error adding files");
						return -1;
					}
					break;
				case m_save:
					if(!list_save(argv[i])){
						perror("Error saving MD5 list");
						return -1;
					}
					break;
				case m_load:
					if(!list_load(argv[i])){
						perror("Error loading MD5 list");
						return -1;
					}
					break;
				case m_remove:
					if(!list_remove(argv[i])){
						perror("Error removing file from list");
						return -1;
					}
					break;
				case m_rmprefix:
					if(!list_rmprefix(argv[i])){
						perror("Error removing prefix on files from list");
						return -1;
					}
					break;
				case m_update:
					if(!list_update(argv[i], follow, nonreg, hashrights)){
						perror("Error updating file from list");
						return -1;
					}
					break;
				case m_undefined:
					usage(argv[0]);
					return -1;
			}
		}
	}

	list_free();
	return r;
}
